// Pipeline Component Library Example
// ----------------------------------
// Colin Bowern (colinbowern@hotmail.com)
// January 2007
//
// Copyright (c) 2007 Colin Bowern
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// * The above copyright notice and this permission notice shall be included in
//   all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

#region References
using Microsoft.CommerceServer.Interop;
using Microsoft.CommerceServer.Runtime;
using Microsoft.Practices.EnterpriseLibrary.Data;
using Microsoft.Practices.EnterpriseLibrary.Data.Sql;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Data.Sql;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
#endregion

namespace RockstarGuys.PipelineComponents
{
    /// <summary>
    /// Calculates the tax for line items and shipments in an order.
    /// </summary>
    [ComVisible(true), Description("RSG.CalculateTax"), ProgId("RSG.CalculateTax")]
    [Guid("01f7bda9-9cb0-458d-95f4-7ac4ca33c4fc")]
    [PipelineCategory(PipelineCategories.CommerceServer, PipelineCategories.AllStages,
                      PipelineCategories.Tax)]
    public class CalculateTax : IPersistDictionary, IPipelineComponent,
                                IPipelineComponentAdmin, IPipelineComponentDescription,
                                ISpecifyPipelineComponentUI
    {
        #region Fields
        private const string ComponentCategory = "CalculateTax";
        private bool isDirty;
        private bool enableTaxExemptionRules;
        private string countryTaxExemptionKey;
        private KeySources countryTaxExemptionKeySource;
        private string regionTaxExemptionKey;
        private KeySources regionTaxExemptionKeySource;
        private Dictionary<string, decimal> taxRateCache = new Dictionary<string, decimal>();
        #endregion

        #region Properties
        /// <summary>
        /// Gets or sets a value indicating whether to enable tax exemption rules.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if tax exemption rules are enabled; otherwise, <c>false</c>.
        /// </value>
        public bool EnableTaxExemptionRules
        {
            get { return this.enableTaxExemptionRules; }
            set
            {
                this.enableTaxExemptionRules = value;
                this.isDirty = true;
            }
        }

        /// <summary>
        /// Gets or sets the country tax exemption key.
        /// </summary>
        /// <value>The country tax exemption key.</value>
        public string CountryTaxExemptionKey
        {
            get { return this.countryTaxExemptionKey; }
            set
            {
                this.countryTaxExemptionKey = value;
                this.isDirty = true;
            }
        }

        /// <summary>
        /// Gets or sets the country tax exemption key source.
        /// </summary>
        /// <value>The country tax exemption key source.</value>
        public KeySources CountryTaxExemptionKeySource
        {
            get { return this.countryTaxExemptionKeySource; }
            set
            {
                this.countryTaxExemptionKeySource = value;
                this.isDirty = true;
            }
        }

        /// <summary>
        /// Gets or sets the region tax exemption key.
        /// </summary>
        /// <value>The region tax exemption key.</value>
        public string RegionTaxExemptionKey
        {
            get { return this.regionTaxExemptionKey; }
            set
            {
                this.regionTaxExemptionKey = value;
                this.isDirty = true;
            }
        }

        /// <summary>
        /// Gets or sets the region tax exemption key source.
        /// </summary>
        /// <value>The region tax exemption key source.</value>
        public KeySources RegionTaxExemptionKeySource
        {
            get { return this.regionTaxExemptionKeySource; }
            set
            {
                this.regionTaxExemptionKeySource = value;
                this.isDirty = true;
            }
        }
        #endregion

        #region Constructor
        /// <summary>
        /// Initializes a new instance of the <see cref="T:CalculateTax"/> class.
        /// </summary>
        public CalculateTax()
        {
            this.InitNew();
        }
        #endregion

        #region Methods
        #region Initialization/State
        /// <summary>
        /// Sets the design mode on the component.
        /// </summary>
        /// <remarks>
        /// Use this method to prepare the component for execution in one of two
        /// modes: design mode or execution mode. In design mode, which is useful
        /// for running the Pipeline Editor, errors are more easily tolerated.
        /// Execution mode (the default) is analogous to production mode.
        /// </remarks>
        /// <param name="fEnable">Indicates whether the component should be run
        /// in design mode (True) or execution mode (False).</param>
        public void EnableDesign(int fEnable) { }

        /// <summary>
        /// Provides an opportunity for an object to initialize its state before
        /// any persisted values are loaded.
        /// </summary>
        public void InitNew()
        {
            this.enableTaxExemptionRules = false;
            this.isDirty = false;
        }

        /// <summary>
        /// Determines whether the configuration properties have changed since
        /// the last save.
        /// </summary>
        /// <returns></returns>
        public int IsDirty()
        {
            return Convert.ToInt32(this.isDirty);
        }
        #endregion

        #region Business Logic
        /// <summary>
        /// The entry point at which the pipeline begins executing the component.
        /// </summary>
        /// <param name="pdispOrder">A pointer to the OrderForm dictionary.</param>
        /// <param name="pdispContext">A pointer to the Context dictionary.
        /// This is a Dictionary object that contains pointers to the
        /// MessageManager object, Content object, and language information.</param>
        /// <param name="lFlags">Reserved</param>
        /// <returns></returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        public int Execute(object pdispOrder, object pdispContext, int lFlags)
        {
            ComponentErrorLevel componentErrorLevel = ComponentErrorLevel.Success;
            Dictionary<string, object> logContext = new Dictionary<string, object>();
            OrderForm orderForm = new OrderForm((IDictionary)pdispOrder);
            PipelineContext pipelineContext = new PipelineContext((IDictionary)pdispContext);

            try
            {
                logContext["OrderGroupId"] = orderForm.OrderGroupId;
                logContext["SoldToId"] = orderForm.SoldToId;

                orderForm.TaxIncluded = decimal.Zero;
                orderForm.TaxTotal = decimal.Zero;

                bool countryTaxExempt = false;
                bool regionTaxExempt = false;

                if ((EnableTaxExemptionRules) &&
                    (CountryTaxExemptionKeySource == KeySources.OrderForm))
                {
                    countryTaxExempt = orderForm.GetValue<bool>(CountryTaxExemptionKey);
                }
                if ((EnableTaxExemptionRules) &&
                    (RegionTaxExemptionKeySource == KeySources.OrderForm))
                {
                    regionTaxExempt = orderForm.GetValue<bool>(RegionTaxExemptionKey);
                }

                foreach (LineItem lineItem in orderForm.LineItems)
                {
                    logContext["LineItemId"] = lineItem.LineItemId;

                    OrderAddress shippingAddress = orderForm.Addresses[lineItem.ShippingAddressId];

                    if ((EnableTaxExemptionRules) &&
                        (CountryTaxExemptionKeySource == KeySources.LineItem))
                    {
                        countryTaxExempt = lineItem.GetValue<bool>(CountryTaxExemptionKey);
                    }
                    if ((EnableTaxExemptionRules) &&
                        (RegionTaxExemptionKeySource == KeySources.LineItem))
                    {
                        regionTaxExempt = lineItem.GetValue<bool>(RegionTaxExemptionKey);
                    }

                    decimal taxRate = RetrieveTaxRate(pipelineContext.CommerceResources.TransactionConfigSqlConnectionString,
                                                      shippingAddress.CountryCode,
                                                      countryTaxExempt,
                                                      shippingAddress.RegionCode,
                                                      regionTaxExempt);

                    logContext["CountryCode"] = shippingAddress.CountryCode;
                    logContext["CountryTaxExempt"] = countryTaxExempt;
                    logContext["RegionCode"] = shippingAddress.RegionCode;
                    logContext["RegionTaxExempt"] = regionTaxExempt;
                    logContext["TaxRate"] = taxRate;
                    logContext["ExtendedPrice"] = lineItem.ExtendedPrice;

                    lineItem.TaxIncluded = decimal.Zero;
                    lineItem.TaxTotal = Helper.Round(orderForm.CurrencyDecimalPlaces,
                                                     lineItem.ExtendedPrice.GetValueOrDefault() * taxRate);

                    logContext["TaxTotal"] = lineItem.TaxTotal;

                    orderForm.TaxTotal += lineItem.TaxTotal;

                    Helper.Log(EventIdentifiers.CalculateTaxApplyingLineItemTax,
                               TraceEventType.Verbose,
                               logContext,
                               CalculateTax.ComponentCategory,
                               StringResources.ApplyingLineItemTax);

                    logContext.Remove("TaxTotal");
                    logContext.Remove("ExtendedPrice");
                    logContext.Remove("TaxRate");
                    logContext.Remove("RegionTaxExempt");
                    logContext.Remove("RegionCode");
                    logContext.Remove("CountryTaxExempt");
                    logContext.Remove("CountryCode");
                    logContext.Remove("LineItemId");
                }

                if ((EnableTaxExemptionRules) &&
                    (CountryTaxExemptionKeySource == KeySources.LineItem))
                {
                    countryTaxExempt = false;
                }
                if ((EnableTaxExemptionRules) &&
                    (RegionTaxExemptionKeySource == KeySources.LineItem))
                {
                    regionTaxExempt = false;
                }

                foreach (Shipment shipment in orderForm.Shipments)
                {
                    logContext["ShipmentId"] = shipment.ShipmentId;

                    OrderAddress shippingAddress = orderForm.Addresses[shipment.ShippingAddressId];
                    decimal taxRate = RetrieveTaxRate(pipelineContext.CommerceResources.TransactionConfigSqlConnectionString,
                                                      shippingAddress.CountryCode,
                                                      countryTaxExempt,
                                                      shippingAddress.RegionCode,
                                                      regionTaxExempt);

                    logContext["CountryCode"] = shippingAddress.CountryCode;
                    logContext["CountryTaxExempt"] = countryTaxExempt;
                    logContext["RegionCode"] = shippingAddress.RegionCode;
                    logContext["RegionTaxExempt"] = regionTaxExempt;
                    logContext["TaxRate"] = taxRate;
                    logContext["ShippingTotal"] = shipment.ShippingTotal;

                    shipment.TaxIncluded = decimal.Zero;
                    shipment.TaxTotal = Helper.Round(orderForm.CurrencyDecimalPlaces,
                                                     shipment.ShippingTotal * taxRate);

                    logContext["TaxTotal"] = shipment.TaxTotal;

                    orderForm.TaxTotal += shipment.TaxTotal;

                    Helper.Log(EventIdentifiers.CalculateTaxApplyingShipmentTax,
                               TraceEventType.Verbose,
                               logContext,
                               CalculateTax.ComponentCategory,
                               StringResources.ApplyingShipmentTax);

                    logContext.Remove("TaxTotal");
                    logContext.Remove("ShippingTotal");
                    logContext.Remove("TaxRate");
                    logContext.Remove("RegionTaxExempt");
                    logContext.Remove("RegionCode");
                    logContext.Remove("CountryTaxExempt");
                    logContext.Remove("CountryCode");
                    logContext.Remove("ShipmentId");
                }
            }
            catch (Exception exception)
            {
                // While you would typically not want to catch general
                // exceptions because of the .NET to COM to .NET interop path
                // useful exception details (e.g. call stack) are lost. To get
                // around this we will log the exception and rethrow it.
                // Throwing an exception is equivalent to returning a Failure
                // status from this method (both set the IErrorInfo and HRESULT
                // on the thread). By throwing the exception unit tests can be
                // written to cover various exceptional circumstances.
                Helper.Log(EventIdentifiers.CalculateTaxUnhandledException,
                           TraceEventType.Error,
                           logContext,
                           CalculateTax.ComponentCategory,
                           exception);
                throw;
            }

            return (int)componentErrorLevel;
        }

        /// <summary>
        /// Retrieves the tax rate.
        /// </summary>
        /// <param name="connectionString">The connection string.</param>
        /// <param name="countryCode">The country code.</param>
        /// <param name="countryTaxExempt">if set to <c>true</c> only taxes
        /// where the countryCode and regionCode have an exact match
        /// (excluding wildcards) will be used.</param>
        /// <param name="regionCode">The region code.</param>
        /// <param name="regionTaxExempt">if set to <c>true</c> only values
        /// where the countryCode matches and regionCode is set to a wildcard
        /// will be used.</param>
        /// <returns>The total percentage of tax to apply.</returns>
        private decimal RetrieveTaxRate(string connectionString, string countryCode, bool countryTaxExempt, string regionCode, bool regionTaxExempt)
        {
            Helper.CheckEmptyString(connectionString, "connectionString");
            Helper.CheckStringFormat(countryCode, "countryCode",
                                     RegularExpressionObjects.CountryRegionCodeFormat, false);
            Helper.CheckStringFormat(regionCode, "regionCode",
                                     RegularExpressionObjects.CountryRegionCodeFormat, true);

            decimal taxRate;
            string cacheKey = String.Format(CultureInfo.InvariantCulture,
                                            "{0}:{1}:{2}:{3}",
                                            countryCode,
                                            countryTaxExempt,
                                            regionCode,
                                            regionTaxExempt);

            if (this.taxRateCache.ContainsKey(cacheKey))
            {
                taxRate = this.taxRateCache[cacheKey];
            }
            else
            {
                SqlDatabase database = new SqlDatabase(connectionString);
                taxRate = Convert.ToDecimal(database.ExecuteScalar("RetrieveTaxRate",
                                                                   countryCode,
                                                                   countryTaxExempt,
                                                                   regionCode,
                                                                   regionTaxExempt),
                                            CultureInfo.InvariantCulture) / 100M;
                this.taxRateCache[cacheKey] = taxRate;
            }

            return taxRate;
        }
        #endregion

        #region Configuration
        /// <summary>
        /// Returns an IDictionary with the configuration data for the component.
        /// </summary>
        /// <returns>IDictionary with the configuration data.</returns>
        public object GetConfigData()
        {
            IDictionary dictionary = new DictionaryClass();
            this.Save(dictionary, Convert.ToInt32(false));
            return dictionary;
        }

        /// <summary>
        /// Loads the configuration data from a dictionary object.
        /// </summary>
        /// <param name="pDict">An IDictionary containing the configuration data.</param>
        public void SetConfigData(object pDict)
        {
            IDictionary dictionary = (IDictionary)pDict;
            this.Load(dictionary);
        }

        /// <summary>
        /// Loads the configuration properties from the dictionary.
        /// </summary>
        /// <param name="pdispDict">The pdisp dict.</param>
        public void Load(object pdispDict)
        {
            CalculateTaxConfiguration configuration = new CalculateTaxConfiguration((IDictionary)pdispDict);

            this.enableTaxExemptionRules = configuration.EnableTaxExemptionRules;
            this.countryTaxExemptionKey = configuration.CountryTaxExemptionKey;
            this.countryTaxExemptionKeySource = configuration.CountryTaxExemptionKeySource;
            this.regionTaxExemptionKey = configuration.RegionTaxExemptionKey;
            this.regionTaxExemptionKeySource = configuration.RegionTaxExemptionKeySource;

            this.isDirty = false;
        }

        /// <summary>
        /// Saves the configuration properties to the provided dictionary.
        /// </summary>
        /// <param name="pdispDict">IDictionary to store configuration values.</param>
        /// <param name="fSameAsLoad">Equal to 1 if pdispDict is the same as the one from which the persisted values of the component were loaded.</param>
        public void Save(object pdispDict, int fSameAsLoad)
        {
            CalculateTaxConfiguration configuration = new CalculateTaxConfiguration((IDictionary)pdispDict);

            configuration.EnableTaxExemptionRules = this.enableTaxExemptionRules;
            configuration.CountryTaxExemptionKey = this.countryTaxExemptionKey;
            configuration.CountryTaxExemptionKeySource = this.countryTaxExemptionKeySource;
            configuration.RegionTaxExemptionKey = this.regionTaxExemptionKey;
            configuration.RegionTaxExemptionKeySource = this.regionTaxExemptionKeySource;

            this.isDirty = false;
        }
        #endregion

        #region Identification
        /// <summary>
        /// Gets the COM programmatic identifier for the pipeline component user interface.
        /// </summary>
        /// <returns>String containing the COM programmatic identifier for the pipeline component user interface.</returns>
        public string GetPipelineComponentUIProgID()
        {
            ProgIdAttribute attribute = (ProgIdAttribute)Attribute.GetCustomAttribute(typeof(CalculateTaxProperties), typeof(ProgIdAttribute));
            return attribute.Value;
        }

        /// <summary>
        /// Returns the COM programmatic identifier
        /// </summary>
        /// <returns>String containing the COM programmatic identifier for this type.</returns>
        public string GetProgID()
        {
            ProgIdAttribute attribute = (ProgIdAttribute)Attribute.GetCustomAttribute(typeof(CalculateTax), typeof(ProgIdAttribute));
            return attribute.Value;
        }

        /// <summary>
        /// Returns a string array that identifies the pipeline context values that
        /// the component reads.
        /// </summary>
        /// <returns>String array of pipeline context keys</returns>
        public object ContextValuesRead()
        {
            List<object> contextValuesRead = new List<object>();

            contextValuesRead.Add(PipelineContext.KeyNames.CommerceResources + "." + CommerceResources.KeyNames.TransactionConfig.ResourceKey + "." + CommerceResources.KeyNames.TransactionConfig.ConnectionString);

            contextValuesRead.TrimExcess();
            object[] returnValue = contextValuesRead.ToArray();
            return returnValue;
        }

        /// <summary>
        /// Returns a string array that identifies the values that the component reads.
        /// </summary>
        /// <returns>String array of order form keys</returns>
        public object ValuesRead()
        {
            List<object> valuesRead = new List<object>();

            valuesRead.Add(OrderForm.KeyNames.OrderGroupId);
            valuesRead.Add(OrderForm.KeyNames.SoldToId);
            valuesRead.Add(OrderForm.KeyNames.CurrencyDecimalPlaces);
            valuesRead.Add(OrderForm.KeyNames.Addresses + "." + OrderAddress.KeyNames.RegionCode);
            valuesRead.Add(OrderForm.KeyNames.Addresses + "." + OrderAddress.KeyNames.CountryCode);
            valuesRead.Add(OrderForm.KeyNames.LineItems + "." + LineItem.KeyNames.LineItemId);
            valuesRead.Add(OrderForm.KeyNames.LineItems + "." + LineItem.KeyNames.ExtendedPrice);
            valuesRead.Add(OrderForm.KeyNames.LineItems + "." + LineItem.KeyNames.ShippingAddressId);
            valuesRead.Add(OrderForm.KeyNames.Shipments + "." + Shipment.KeyNames.ShipmentId);
            valuesRead.Add(OrderForm.KeyNames.Shipments + "." + Shipment.KeyNames.ShippingAddressId);
            valuesRead.Add(OrderForm.KeyNames.Shipments + "." + Shipment.KeyNames.ShippingTotal);

            if ((EnableTaxExemptionRules) && (CountryTaxExemptionKeySource == KeySources.OrderForm))
            {
                valuesRead.Add(this.CountryTaxExemptionKey);
            }
            if ((EnableTaxExemptionRules) && (CountryTaxExemptionKeySource == KeySources.LineItem))
            {
                valuesRead.Add(OrderForm.KeyNames.LineItems + "." + this.CountryTaxExemptionKey);
            }
            if ((EnableTaxExemptionRules) && (RegionTaxExemptionKeySource == KeySources.OrderForm) && (!valuesRead.Contains(this.RegionTaxExemptionKey)))
            {
                valuesRead.Add(this.RegionTaxExemptionKey);
            }
            if ((EnableTaxExemptionRules) && (RegionTaxExemptionKeySource == KeySources.LineItem) && (!valuesRead.Contains(OrderForm.KeyNames.LineItems + "." + this.RegionTaxExemptionKey)))
            {
                valuesRead.Add(OrderForm.KeyNames.LineItems + "." + this.RegionTaxExemptionKey);
            }

            valuesRead.TrimExcess();
            object[] returnValue = valuesRead.ToArray();
            return returnValue;
        }

        /// <summary>
        /// Returns a string array that identifies the values that the component writes.
        /// </summary>
        /// <returns>String array of order form keys</returns>
        public object ValuesWritten()
        {
            List<object> valuesWritten = new List<object>();

            valuesWritten.Add(OrderForm.KeyNames.TaxIncluded);
            valuesWritten.Add(OrderForm.KeyNames.TaxTotal);
            valuesWritten.Add(OrderForm.KeyNames.LineItems + "." + LineItem.KeyNames.TaxIncluded);
            valuesWritten.Add(OrderForm.KeyNames.LineItems + "." + LineItem.KeyNames.TaxTotal);
            valuesWritten.Add(OrderForm.KeyNames.Shipments + "." + Shipment.KeyNames.TaxIncluded);
            valuesWritten.Add(OrderForm.KeyNames.Shipments + "." + Shipment.KeyNames.TaxTotal);

            valuesWritten.TrimExcess();
            object[] returnValue = valuesWritten.ToArray();
            return returnValue;
        }
        #endregion
        #endregion

        #region Nested Types
        /// <summary>
        /// Key Sources
        /// </summary>
        public enum KeySources
        {
            /// <summary>
            /// Not set
            /// </summary>
            None,
            /// <summary>
            /// Order Form
            /// </summary>
            OrderForm,
            /// <summary>
            /// Line Item
            /// </summary>
            LineItem,
        }
        #endregion
    }
}
